from ..log import log_gui
from .. import screen
from ..inertia import InertialMover
from statemachine import ListWidgetStateMachine
import time
import gtk
import gobject
import random

def DrawWhiteRectangle(window, x, y, w, h):
  cmap = window.get_colormap()
  color = cmap.alloc_color('#FFFFFF')
  colorgc = window.new_gc()
  colorgc.set_foreground(color)
  colorgc.set_fill(gtk.gdk.SOLID)

  window.draw_rectangle(
      colorgc, True, x, y, w, h)

def DrawBlueRectangle(window):
  cmap = window.get_colormap()
  color = cmap.alloc_color('#00' + str(int(random.random() * 10)) + '0FF')
  colorgc = window.new_gc()
  colorgc.set_foreground(color)
  colorgc.set_fill(gtk.gdk.SOLID)

  window.draw_rectangle(
      colorgc, True, 0, 0, 800, 400)

def DrawSmallRedRectangle(window, x, y):
  cmap = window.get_colormap()
  color = cmap.alloc_color('#FF' + str(int(random.random() * 10)) + '000')
  colorgc = window.new_gc()
  colorgc.set_foreground(color)
  colorgc.set_fill(gtk.gdk.SOLID)

  window.draw_rectangle(
      colorgc, True, x, y, 15, 15)

def DrawRedRectangle(window, x=0):
  cmap = window.get_colormap()
  color = cmap.alloc_color('#FF' + str(int(random.random() * 10)) + '000')
  colorgc = window.new_gc()
  colorgc.set_foreground(color)
  colorgc.set_fill(gtk.gdk.SOLID)

  window.draw_rectangle(
      colorgc, True, x, 0, 800, 400)

class ListWidget(screen.BaseScreenWidget):
  DRAG_THRESHOLD = 25

  def __init__(self, x, y, width, height):
    screen.BaseScreenWidget.__init__(self, x, y, width, height)
    self._state_machine = ListWidgetStateMachine(self)
    self.red = -1

    # Friction is in pixels per second^2.
    self.friction = 1700.0

    # Distance between items in pixels.
    self.separator_height = 1

    # Color of separators between items and above top and bottommost item.
    self.separator_color = '#E0E0E0'

    # A cached copy of the parsed color allocated from the cache's colormap.
    self._separator_cmap_color = None

    # ButtonUp is not reliable because the touchscreen on Maemo is not sensitive
    # enough. Thus, any time we get a button up event, we debounce it
    # (de-un-bounce, really). This timer waits for 100ms to make sure you really
    # meant to take your finger off the touchscreen before committing to a tap.
    self._debounce_timer = None

    # When auto-scrolling, and update is performed every 10ms. This is the timer
    # that drives that.
    self._update_timer = None

    # The index into self._items of the currently highlighted item (e.g., the
    # item the user pressed on--though it may turn out not to be a tap if he
    # starts dragging).
    self._selected_index = None

    # The cache stores a copy of the image for this widget. All item draws are
    # done to the cache and then the cache is blitted to the window. I would
    # prefer to draw directly to the window's backing store and eliminate the
    # cache, but it looks like GTK throws out its backing store when
    # window.end_paint() is called.
    self._cache = None

    # A set of item indices telling which have been drawn to the image cache.
    self._drawn_items = set()

    # A set of item indices telling which need to be drawn to the image cache.
    self._visible_items = set()

    # Indices of items that need to be drawn on the next update.
    self._dirty_items = []

    # The offset at which the last draw occurred. This is used to move blocks of
    # pixels in the cache to scroll, rather than blitting each item to the
    # cache.
    self._prev_draw_offset = None

    # A list of BaseListItem objects, ordered from the top of the list to the
    # bottom.
    self._items = []

    # The y coordinate where the mouse was first pressed on a drag. This is
    # only used to tell if you've dragged far enough that the currently
    # highlighted item should be un-highlighted.
    self._initial_drag_position = None

    # Track this list's inertia. All the physics is factored out into this
    # module which is updated with positions and times and then can provide
    # recommended speeds which decrease subject to friction (pix/sec^2).
    self._inertia = InertialMover(self.friction)

    # Offset is negative when scrolled toward the bottom of the list.
    # It indicates how many pixels past the top of the first item you have
    # scrolled (i.e., -1 * how many pixels are above the top of the screen).
    self._offset = 0

    # All items are assumed to have the same height. This value is in pixels.
    self._item_height = 0

  def GetSelectedItem(self):
    if self._selected_index is None:
      return None
    return self._items[self._selected_index]

  def HighlightCurrentAndSaveCoordsEvent(self, event, data):
    """Called on entering the init_sel state.

    If you pressed on an item, set it highlighted. Save the position you
    pressed on to use as a reference for future drags. Remove momentum from
    the physics engine and initialize it with the press position."""
    self._enter_init_sel_time = time.time()

    # Find the item you pressed on. Returns None if you pressed on an area
    # of the list widget without an item (e..g, because of a short list).
    self._selected_index = self.FindItemAtCoords(
        self._down_coords[0], self._down_coords[1])

    # If you pressed on an item, invert its color and note that it should
    # be redrawn.
    if self._selected_index is not None:
      self._items[self._selected_index].SetHighlighted(True)
      self._dirty_items.append(self._selected_index)
      needs_paint = True
    else:
      needs_paint = False

    # Prepare the physics engine for a drag in case a mousemove comes later.
    # Save the initial drag position to help compute when a move is big and
    # the item should be un-highlighted.
    y = self._down_coords[1]
    self._inertia.TeleportWithoutMomentum(y, time.time())
    self._initial_drag_position = y

    # Redraw the highlighted item if necessary.
    if needs_paint:
      self.parent.Paint()

  def SaveCoordsEvent(self, event, data):
    """Called when the button is pressed in the normal or autoscroll state.

    This saves the coordinates at which the mouse was pressed so that the next
    state (which will be init_sel) can highlight the correct event. Also stops
    auto-scrolling because we're entering a drag mode."""
    self._down_coords = (data[0], data[1])
    log_gui("**** SAVECOORDSEVENT")
    self.StopAutoScroll()

  def TestForBigMoveEvent(self, event, data):
    """Called when the mouse moves in init_sel.

    Performs a drag and then tests if you've scrolled far enough that it's no
    longer possible for this to be a tap--it must be a drag."""
    y = data[1]
    self.Drag(y)
    if abs(y - self._initial_drag_position) > ListWidget.DRAG_THRESHOLD:
      self._state_machine.Event(ListWidgetStateMachine.EVENT_BIG_MOVE)
    else:
      self.parent.Paint()

  def CancelOrStartDebounceEvent(self, event, data):
    """Called on enter to the maybe_tap state. Send a cancel event if not enough
    time has passed since button down; else start the debounce timer.

    Starts the debounce timer."""
    elapsed_time = time.time() - self._enter_init_sel_time
    if elapsed_time < 0.09:
      log_gui("inertia2: Not enough time since enter init sel:", elapsed_time)
      self.UnhighlightSelected()
      self.parent.Paint()
      self._state_machine.Event(ListWidgetStateMachine.EVENT_CANCEL)
    else:
      log_gui("inertia2: Adequate time since enter init sel:", elapsed_time)
      self.StartDebounce()

  def CancelDebounceEvent(self, event, data):
    """Called on button down in maybe_tap. Also on exit, debounce timout from
    maybe_drop.

    Stop the debounce timer."""
    self.CancelDebounce()

  def TapEvent(self, event, data):
    """Called on enter to the tap state.

    Commits that an item was tapped. Run the item's OnTap callback and
    unhighlight it conditioned on its return code."""
    if self._selected_index is None:
      self.ActionDone()
      return
    if self._items[self._selected_index].OnTap() is None:
      self.UnhighlightSelected()
      self.parent.Paint()
      self.ActionDone()
    else:
      log_gui("*** Asynch termination of OnTap. Expecting ActionDone later.")

  def UnhighlightEvent(self, event, data):
    """Called on enter to the dragging state and normal state.

    Unhighlight the selected item."""
    self.UnhighlightSelected()
    self.parent.Paint()

  def DragEvent(self, event, data):
    """Called on move in the dragging state.

    Drag the list."""
    self.Drag(data[1])
    self.parent.Paint()

  def UpWhileDragEvent(self, event, data):
    """Called on button up in the dragging state.

    Stops autoscrolling if there hasn't been a move in a while."""
    # If the mouse has been still for a while then assume the user's finger
    # stopped moving for a while before going up, so it shouldn't autoscroll.
    if self._inertia.CanInferStillness(time.time()):
      self.StopAutoScroll()

  def StartDebounceAndUpdateEvent(self, event, data):
    """Called on enter to the maybe_drop state.

    Begin regular updates for autoscrolling and also start a debounce timer."""
    self.StartDebounce()
    self.StartUpdate()

  def CancelUpdateEvent(self, event, data):
    """Called on button down from dragging and on speed=0 from autoscroll.

    Stop running the autoscroll update timer."""
    self.CancelUpdate()

  def UpdateEvent(self, event, data):
    """Called on update timeout from autoscroll and maybe_drop.

    Update the internal state and repaint for autoscrolling."""
    self.Update()
    self.parent.Paint()

  def UnhighlightSelected(self):
    """If an item is currently highlighted, unhighlight it."""
    if self._selected_index is not None:
      self._items[self._selected_index].SetHighlighted(False)
      self._dirty_items.append(self._selected_index)

  def ActionDone(self):
    self._state_machine.Event(ListWidgetStateMachine.EVENT_ACTION_DONE)

  def CancelDebounce(self):
    """Stop running the debounce timer."""
    if self._debounce_timer:
      gobject.source_remove(self._debounce_timer)
      self._debounce_timer = None

  def CancelUpdate(self):
    """Stop running the autoscroll update timer."""
    if not self._update_timer:
      return
    log_gui("*** CANCEL UPDATE")
    gobject.source_remove(self._update_timer)
    self._update_timer = None
    self._inertia.Reset()

  def StartUpdate(self):
    """Begin running the autoscroll update timer."""
    log_gui("*** START UPDATE")
    if self._update_timer:
      return

    def FireUpdate():
      # Run all the pending events.
      while gtk.events_pending():
        gtk.main_iteration(block=True)
      self._state_machine.Event(ListWidgetStateMachine.EVENT_UPDATE_TIMEOUT)
      return True

    self._update_timer = gobject.timeout_add(17, FireUpdate)

  def StartDebounce(self):
    """Start running the debounce timer."""
    assert not self._debounce_timer

    def FireDebounce():
      self._state_machine.Event(ListWidgetStateMachine.EVENT_DEBOUNCE_TIMEOUT)
      self._debounce_timer = None
      return False

    self._debounce_timer = gobject.timeout_add(200, FireDebounce)

  def Update(self):
    """Update internal state (e.g., autoscrolling)."""
    now = time.time()
    dy = int(self._inertia.Update(now))
    if dy:
      self.ScrollBy(dy)
    if self._inertia.IsStopped():
      self.CancelUpdate()
    self.parent.Paint()

  def StopAutoScroll(self):
    """Stop autoscrolling. Reset speed and all other physics engine state.
    Cancels the update timer in case it is running."""
    self._inertia.Reset()
    self.CancelUpdate()

  def Drag(self, y):
    """Pass in the mouse position in y. This moves the window and keeps track
    of state. The caller needs to call self.Paint() after this."""
    now = time.time()
    if not self._inertia.IsInitialized():
      self._inertia.AbsoluteMove(y, now)
      return

    dy = int(self._inertia.AbsoluteMove(y, now))
    if dy:
      self.ScrollBy(dy)

  def FindItemAtCoords(self, x, y):
    """Returns the index of the item at the coords relative to the list widget's
    origin. Returns None if there's no item where you tapped."""
    index = int((y - self._offset - self.rect.top) / self._item_height)
    if index >= 0 and index < len(self._items):
      return index
    else:
      return None

  def ScrollBy(self, dy):
    """Adjust the offset appropriately given a relative scroll of dy pixels.
    Also stops autoscroll if you ran into the top or bottom of the list."""
    orig_offset = self._offset
    self._offset += dy
    if self._offset > 0:
      self._offset = 0
      self.StopAutoScroll()
    min_offset = min(0,
        -(len(self._items) * self._item_height - self.rect.height))
    if self._offset < min_offset:
      self._offset = int(min_offset)
      self.StopAutoScroll()

  def AddItem(self, item):
    """Add an item to the list. This must not be called after the first draw."""
    if not self._items:
      self._item_height = item.height + self.separator_height
    self._dirty_items.append(len(self._items))
    self._items.append(item)
    item.OnAdd(self)

  def Draw(self, screen, window, clip_rect):
    """Draw the list to the buffer window."""
    if not self._cache:
      # Create a new backing store.
      log_gui("New backing store")
      self._cache = gtk.gdk.Pixmap(
          window, self.rect.width, self.rect.height, -1)
      gc = screen.GC(window)
      cmap = window.get_colormap()
      color = cmap.alloc_color('#FFFFFF')
      gc.set_foreground(color)
      gc.set_fill(gtk.gdk.SOLID)
      self._cache.draw_rectangle(gc, True, 0, 0, self.rect.width,
          self.rect.height)

    if self._prev_draw_offset is not None:
      # This is not the first draw so scroll. Also update the list of items
      # that need to be drawn for the first time because they were just exposed
      # by a scroll.
      dy = self._offset - self._prev_draw_offset
      log_gui("offset change")
      self.ScrollCacheBy(self._cache, dy)
      self.AddExposedDirtyItems(dy)
    else:
      # Add visible items to the set of dirty items.
      self.AddExposedDirtyItems(0)

    # Draw any items that have changed or just been exposed.
    self.DrawDirtyItems(screen, self._cache, clip_rect)

    # Copy the backing store to the window.
    self.CopyCacheToWindow(self._cache, window, clip_rect, screen)

    # Update internal state. The prev_draw_offset is needed to know how much to
    # scroll on the next call to this function.
    self._prev_draw_offset = self._offset
    self._dirty_items = []

  def CopyCacheToWindow(self, cache, window, clip_rect, screen):
    """Copy the backing store to the parent's window."""
    log_gui("Draw", self, "to", self.rect, "with clipping", clip_rect, "screen rect", screen.rect)

    gc = screen.GC(window)
    if clip_rect:
      gc.set_clip_rectangle(clip_rect.GetGdkRectangle())
    window.draw_drawable(
        gc,
        cache,
        0,
        0,
        self.rect.left + screen.rect.left,
        self.rect.top + screen.rect.top,
        self.rect.width,
        self.rect.height)

  def AddExposedDirtyItems(self, dy):
    """At the current offset, which items in the bottom/top dy pixels are
    newly exposed? Mark those dirty. Also updates the set of visible items."""
    # Update the set of visible items.
    first_visible_index, last_visible_index = self.GetVisibleIndices()
    self._visible_items = set(range(first_visible_index, last_visible_index))

    # Add visible items that are not in the set of drawn items to the list of
    # dirty items.
    log_gui("AddExposed: visible=", self._visible_items, "drawn=", self._drawn_items)
    for i in self._visible_items.difference(self._drawn_items):
      log_gui("  ",i,"has been exposed")
      self._dirty_items.append(i)
    # Add items that were partially exposed before and are more revealed now.
    # dy may be zero on the very first draw.
    if dy:
      if dy > 0:
        # Scrolling towards top of list. Exposing items at top.
        old_offset = self._offset - dy
        y = -old_offset
        i = int(y / self._item_height)
      elif dy < 0:
        # Scrolling towards bottom of list. Exposing items at bottom.
        old_offset = self._offset - dy
        y = self.rect.height - old_offset
        i = int(y / self._item_height)
      log_gui("AddExposed: Old offset was", old_offset, "with index", i, "at the top.")
      self._dirty_items.append(i)


  def ScrollCacheBy(self, cache, dy):
    """Scroll the backing store up or down by dy pixels."""
    # Move the bottom/top (self.rect.height-dy) pixels by dy pixels.
    if not dy:
      return
    gc = self.parent.GC(cache)
    if dy > 0:
      # Scrolling toward top of list. Copy from smaller to larger y coords.
      source_y = 0
      dest_y = dy
    else:
      # Scrolling toward bottom of list. Copy from larger to smaller y coords.
      source_y = -dy
      dest_y = 0

    # Copy a rectangle from cache to cache.
    cache.draw_drawable(
        gc,
        cache,
        0,  # source x
        source_y,  # source y
        0,  # dest x
        dest_y,  # dest y
        self.rect.width,  # width to copy
        self.rect.height - abs(dy))  # height to copy

  def DrawDirtyItems(self, screen, cache, clip_rect):
    """Draw items in the dirty list to the cache. Also cleans up the
    set of drawn items to remove anything invisible."""
    # Remove invisible items from the set of drawn items.
    self._drawn_items.intersection_update(self._visible_items)
    log_gui("DrawDirty: Setting drawn items to", self._drawn_items)
    base_y = self._offset
    separator_gc = screen.GC(cache)
    if not self._separator_cmap_color:
      cmap = cache.get_colormap()
      self._separator_cmap_color = cmap.alloc_color(self.separator_color)
    separator_gc.set_foreground(self._separator_cmap_color)
    for item_index in self.GetDirtyItemIndices():
      item = self._items[item_index]
      y = base_y + item_index * self._item_height
      log_gui("Draw item", item_index, "at", y, "with offset", self._offset)
      item.Draw(self, screen, cache, 0, y, cache)
      separator_y = y + self._item_height - self.separator_height
      cache.draw_line(
          separator_gc, 0, separator_y, self.rect.width - 1, separator_y)
      #DrawWhiteRectangle(cache, 0, y, 800, y+self._item_height)
      #DrawSmallRedRectangle(cache, item_index*10, y)
      self._drawn_items.add(item_index)

  def GetVisibleIndices(self):
    """Returns indices of visible items as a tuple:
      (first visible, last visible + 1)"""
    first_visible_index = int(max(0, -self._offset / self._item_height))

    # last_visible_index is actually one past the last visible index because
    # range() takes half-open intervals.
    #
    # When you scroll to show the empty area above the top of the list then
    # offset is positive.
    # As you scroll toward the bottom of the list, it is negative.
    last_visible_index = int(min(
        int((self.rect.height - self._offset) / self._item_height) + 1,
        len(self._items)))
    return first_visible_index, last_visible_index

  def GetDirtyItemIndices(self):
    """Return a set of indices of items that need to be drawn."""
    return set(self._dirty_items).intersection(self._visible_items)

  def OnButtonDown(self, event, data):
    """Called when the button is pressed down."""
    self._state_machine.Event(ListWidgetStateMachine.EVENT_DOWN,
        (event.x, event.y))

  def OnButtonUp(self, event, data):
    """Called when the button is released."""
    x, y, state = event.window.get_pointer()
    point = (x, y)
    self._state_machine.Event(ListWidgetStateMachine.EVENT_UP, point)

  def OnMouseMove(self, event, data):
    """Called when the pointer is moved."""
    x, y, state = event.window.get_pointer()
    self._state_machine.Event(ListWidgetStateMachine.EVENT_MOVE, (x, y))

  def OnUnrealized(self, event, data):
    """Widget was unrealized. Reset it to a neutral state."""
    self.CancelDebounce()
    self.CancelUpdate()
    self.UnhighlightSelected()
    self._inertia.Reset()
    self._state_machine.Reset()
